#include "fat.h"

#include <mm/valloc.h>

#include <device/block.h>

typedef struct {
    void* data;
    int found;
} iterate_ctx;

typedef int (dir_entry_callback)(ldire_t*, sdire_t*, unsigned, iterate_ctx*);

extern int fat_unicode_name_parse(char* dst, ldire_t* src);
extern int fat_name_compare(dentry_t* dentry, ldire_t* name);

static void fat_dirent_assign(dirent_t* dir, ldire_t* name, int type) {
    dir->d_type = type;
    dir->d_nlen = fat_unicode_name_parse(dir->d_name, name);
}

static int fat32_iterate_directory(inode_t* inode, dir_entry_callback* callback, iterate_ctx* context) {
    if (inode->type != FS_DIR) {
        return EINVAL;
    }

    fat_inode_t* finode = inode->data;
    fat_sb_t* fsb = inode->sb->data;
    void* buf = valloc(fsb->cluster_size);
    if (!buf) {
        return ENOMEM;
    }

    int errno = ENOENT;
    u32 cluster = finode->start;
    unsigned d_offset = 0;
    do {
        int ret = fat_get_cluster(inode, cluster, buf);
        if (ret < 1) {
            errno = ENOENT;
            break;
        }

        for (size_t offset = 0; offset < fsb->cluster_size; offset += sizeof(sdire_t)) {
            ldire_t* ldire = (ldire_t*)(buf + offset);
            if (ldire->order == 0 || ldire->order == 0xE5 || ldire->order == '.') {
                continue;
            }

            if (ldire->flag != LFN) {
                errno = EINVAL;
                goto cleanup;
            }

            if ((ldire->order & (~0x40)) > 1) {
                errno = ENAME2LONG;
                goto cleanup;
            }

            offset += sizeof(ldire_t);
            sdire_t* sdire = (sdire_t*)(buf + offset);

            int result = callback(ldire, sdire, d_offset, context);
            if (result != 0) {
                errno = result;
                goto cleanup;
            }
            d_offset++;
        }

        fat_next_clus(inode, cluster, &cluster);
    } while (cluster < FAT32_CLUSTER_END);

cleanup:
    vfree(buf);
    return errno;
}

static int lookup_cb(ldire_t* ldire, sdire_t* sdire, unsigned d_off, iterate_ctx* ctx) {
    dentry_t* dentry = (dentry_t*)ctx->data;
    if (!fat_name_compare(dentry, ldire)) {
        return 0;
    }

    dentry->inode = fs_alloc_inode(dentry->inode->sb);
    if (!dentry->inode) {
        return ENOMEM;
    }

    fs_inode_addhash(dentry->inode);
    fs_assign_inode(dentry, dentry->inode);

    fat_fill_inode(dentry->inode, sdire);

    ctx->found = 1;
    return 1;
}

int fat32_inode_lookup(inode_t* inode, dentry_t* dentry) {
    iterate_ctx context = { .data = dentry, .found = 0 };

    dentry->inode = inode;
    int err = fat32_iterate_directory(inode, lookup_cb, &context);

    if (context.found) {
        return 0;
    }
    dentry->inode = NULL;
    return err;
}

static int readdir_cb(ldire_t* ldire, sdire_t* sdire, unsigned d_off, iterate_ctx* ctx) {
    dirent_t* dirent = (dirent_t*)ctx->data;
    if (d_off == dirent->d_offset) {
        fat_dirent_assign(dirent, ldire, fat_get_dtype(sdire->flag));
        ctx->found = 1;
        return 1;
    }
    return 0;
}

int fat32_file_readdir(file_t* file, dirent_t* dirent) {
    iterate_ctx context = {
        .data = dirent,
        .found = 0
    };
    dirent->d_offset -= 2;
    int err = fat32_iterate_directory(file->inode, readdir_cb, &context);
    dirent->d_offset += 2;

    if (context.found) {
        return 1;
    }
    return err;
}

int fat32_inode_rmdir(inode_t* this, dentry_t* dir) {
    return ENOSET;
}

int fat32_inode_mkdir(inode_t* this, dentry_t* dir) {
    return ENOSET;
}